"""WMS连接基础功能"""

import re
import json
import logging
import datetime
import hashlib
from xml.etree import ElementTree as ET
from base64 import b64encode, b64decode
from urllib.parse import quote, unquote, urlencode
from functools import wraps

from odoo import models, api, fields
from odoo.tools import float_compare
from odoo.exceptions import UserError, ValidationError

import requests

from .. import API_DICT, ERROR_CODE

_logger = logging.getLogger(__name__)
DATE_FORMAT = '%Y-%m-%d %H:%M:%S'   # 从配置信息读取？


class DateEncoder(json.JSONEncoder):
    def default(self, obj):
        """date_time转化方法"""
        if isinstance(obj, (datetime.datetime, datetime.date)):
            return obj.strftime(DATE_FORMAT)
        else:
            return super().default(obj)


class DateDecoder(json.JSONDecoder):
    def default(self, obj):
        # if re.match('^\d{4}-\d{2}-\d{2}$', obj):
        #     return datetime.datetime.strptime(obj, '%Y-%m-%d')
        if re.match('^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}$', obj):
            return datetime.datetime.strptime(obj, DATE_FORMAT)
        else:
            return super().default(obj)


class WMSConnectMixin(models.AbstractModel):
    """通用字段"""
    _name = 'nd.wms'
    _inherit = ['nd.data.converter']
    _description = 'WMS连接功能'

    sync_state = fields.Selection([('success', '同步成功'), ('fail', '同步失败')], string="和WMS的同步状态")
    latest_sync_success_time = fields.Datetime('最近同步成功时间')
    latest_sync_time = fields.Datetime('最近同步尝试时间')
    sync_fail_reason = fields.Char('同步失败原因')

    @staticmethod
    def generate_xml_from_dict(data):
        root = ET.Element('xml')
        for key, value in data.items():
            ET.SubElement(root, key).text = value

        return ET.tostring(root, encoding='utf-8').decode('utf-8')

    @staticmethod
    def parse_xml_to_dict(element_str):
        """将xml格式的element字符串转化成字典"""
        def internal_iter(tree, accum):
            """递归性的解析tree，将其添加到字段accum
            tree：ET.Element，待解析的节点
            accum: dict，解析结果存储字典
            """
            if tree is None:
                return accum
            if list(tree):
                accum[tree.tag] = {}
                for each in list(tree):
                    result = internal_iter(each, {})
                    if each.tag in accum[tree.tag]:
                        if not isinstance(accum[tree.tag][each.tag], list):
                            accum[tree.tag][each.tag] = [
                                accum[tree.tag][each.tag]
                            ]
                        accum[tree.tag][each.tag].append(result[each.tag])
                    else:
                        accum[tree.tag].update(result)
            else:
                accum[tree.tag] = tree.text

            return accum

        if element_str.startswith('<html>'):
            return {}

        element = ET.fromstring(element_str)
        dict_data =  internal_iter(element, {})
        if 'xml' in dict_data:
            return dict_data['xml']
        else:
            return dict_data

    def wms_sync_log_success(self, head_data):
        """记录成功同步"""
        _logger.info(f'数据同步成功: header: {head_data}')
        self.with_context(no_sync=True).write({
            'sync_state': 'success',
            'latest_sync_success_time': fields.Datetime.now(),
            'latest_sync_time': fields.Datetime.now(),
        })

    def wms_sync_log_fail(self, msg, head_data):
        """记录同步失败"""
        _logger.warning(f'数据同步失败: {msg}\nheader: {head_data} ')
        self.with_context(no_sync=True).write({
            'sync_state': 'fail',
            'latest_sync_time': fields.Datetime.now(),
            'sync_fail_reason': msg
        })

    def post_process_and_send_data(e_id):
        def receive_method(f):
            @wraps(f)
            def wrapper(self, *args, **kws):
                if self.env['ir.config_parameter'].sudo().get_param('nd.wms.api.use_wms', default=False) not in ['True']:
                    _logger.info('暂未启用接口，不会同步数据到WMS！')
                    return
                data_to_send = f(self, *args, **kws)    # 如果方法里仍需要返回的数据，就试用yield
                if not data_to_send:
                    return
                wms_url = self._check_api_args(e_id)
                method_data = self._struct_public_args(e_id)
                method_data['timestamp'] = fields.Date.today()
                method_data['data'] = json.dumps(data_to_send, cls=DateEncoder)  # 这里的数据是序列化后的，这样才能加签
                method_data['sign'] = self._sign_data(data_to_send)
                try:
                    headers = {'Content-Type': 'application/x-www-form-urlencoded'}
                    response = requests.post(wms_url, params=method_data, headers=headers)

                    if response.status_code != requests.codes.ok:
                        response.raise_for_status()

                    resp_text = unquote(response.text)
                    resp_dict = json.loads(resp_text)
                    # response = response.json()  # 已经反序列化了。
                    # 这里的响应结果分两种，一种是状态返回，一种是信息返回。后者一般对应于查询，直接认为是成功的
                    return_info = resp_dict.get('Response', {}).get('return')
                    if not return_info:
                        raise UserError('WMS系统没有返回信息')  # TODO 待优化
                    else:
                        if return_info.get('returnFlag') == '0':
                            self.wms_sync_log_fail(return_info.get("returnDesc", ""), method_data)
                            raise UserError(f'同步失败：{return_info.get("returnDesc", "")}')
                        elif return_info.get('returnFlag') == '2':
                            self.wms_sync_log_fail(return_info.get("returnDesc", ""), method_data)
                            raise UserError(f'部分内容同步失败：{return_info.get("returnDesc", "")}，错误列表：{return_info.get("resultInfo", "")}')
                        else:
                            self.wms_sync_log_success(method_data)
                            return UserError('同步成功')    # TODO 待优化

                except UserError as e:
                    raise e
                except Exception as e:
                    raise UserError(f'发送信息至WMS系统时出现错误：{str(e)}')

            return wrapper
        return receive_method

    def _check_api_args(self, e_id):
        """检查WMS配置和接口e_id相关的信息是否完整"""
        api_record = self.env['nd.wms.api'].search([('e_id', '=', e_id)], limit=1)
        wms_url = self.env['ir.config_parameter'].sudo().get_param('nd.wms.api.api_url', default=False)
        client_customerid = self.env['ir.config_parameter'].sudo().get_param('nd.wms.api.client_customerid', default=False)
        app_token = self.env['ir.config_parameter'].sudo().get_param('nd.wms.api.app_token', default=False)
        app_key = self.env['ir.config_parameter'].sudo().get_param('nd.wms.api.app_key', default=False)
        # if not all([api_record.api_method, api_record.client_db, api_record.messageid, wms_url, client_customerid, app_token, app_key]):
        #     raise UserError('WMS接口信息配置不完整！')
        return wms_url

    def _struct_public_args(self, e_id):
        """构建公共参数
        e_id: str: 参考API_DICT的key值
        """
        api_record = self.env['nd.wms.api'].search([('e_id', '=', e_id)], limit=1)
        public_data = {
            'client_customerid': self.env['ir.config_parameter'].sudo().get_param('nd.wms.api.client_customerid', default=False),
            'apptoken': self.env['ir.config_parameter'].sudo().get_param('nd.wms.api.app_token', default=False),
            'appkey': self.env['ir.config_parameter'].sudo().get_param('nd.wms.api.app_key', default=False),
            # 'format': 'json',
        }
        method_data = {
            'method': api_record.api_method,
            'client_db': api_record.client_db,
            'messageid': api_record.messageid,
        }
        method_data.update(public_data)
        return method_data

    def _sign_data(self, data):
        """加签"""
        app_secret = self.env['ir.config_parameter'].sudo().get_param('nd.wms.api.app_secret', default=False)
        serialized_data = json.dumps(data, cls=DateEncoder)
        hl = hashlib.md5()
        encoded_data = (app_secret + serialized_data + app_secret).encode('utf-8')
        hl.update(encoded_data)
        hashed_data = hl.hexdigest()
        upper_b64_data = b64encode(hashed_data.encode()).upper()
        urlencoded_data = quote(upper_b64_data)
        return urlencoded_data

    @post_process_and_send_data('product')
    def push_products_to_wms(self, products):
        """向WMS推送产品资料"""
        # TODO FreightClass意义不明,FirstOP,Alternate_SKU3就是单位?
        datas = self.odoo_product_to_wms(products)
        return datas

    @post_process_and_send_data('partner')
    def push_partners_to_wms(self, partners):
        """向WMS推送客商资料"""
        # TODO ,联系人上的联系人信息.
        datas = self.odoo_partners_to_wms(partners)
        return datas

    def __check_pick_type(self, picks, target_type):
        """
        检查给定的调拨单是否是指定的target_type类型
        :param picks: stock.picking，调拨单
        :param target_type: 待筛选的类型 'incoming, outgoing, internal'
        :return:
        """
        if set(picks.mapped('picking_type_code')) != set([target_type]):
            raise UserError(f'待传输的调拨单类型不符合相关接口要求：{target_type}')

    @post_process_and_send_data('in_stock')
    def push_in_stocks_to_wms(self, pickings):
        """向WMS推送入库单"""
        self.__check_pick_type(pickings, 'incoming')
        datas = self.odoo_in_pickings_to_wms(pickings)
        return datas

    @post_process_and_send_data('in_stock')
    def push_sale_return_to_wms(self, pickings):
        """向WMS推送采购退货入库单(也就是类型为RT的入库单)"""
        self.__check_pick_type(pickings, 'incoming')
        datas = self.odoo_in_pickings_to_wms(pickings, order_type='RT')
        return datas

    @post_process_and_send_data('out_stock')
    def push_out_stocks_to_wms(self, pickings):
        """向WMS系统推送出库单"""
        self.__check_pick_type(pickings, 'outgoing')
        datas = self.odoo_out_pickings_to_wms(pickings)
        return datas

    @post_process_and_send_data('out_stock')
    def push_consignment_stocks_to_wms(self, pickings):
        """向WMS系统推送寄售出库单"""
        # 寄售单的出库单是内部调拨单，而出库单则不同步
        self.__check_pick_type(pickings, 'internal')
        datas = self.odoo_out_pickings_to_wms(pickings, order_type='JS')
        return datas

    @post_process_and_send_data('in_stock')
    def push_consignment_stocks_cancel_to_wms(self, pickings):
        """向WMS系统推送寄售退回单"""
        self.__check_pick_type(pickings, 'internal')
        datas = self.odoo_in_pickings_to_wms(pickings, order_type='JH')
        return datas

    @post_process_and_send_data('out_stock')
    def push_purchase_return_to_wms(self, pickings):
        """向WMS系统采购退货单(也就是类型为PT的出库单)"""
        self.__check_pick_type(pickings, 'outgoing')
        datas = self.odoo_out_pickings_to_wms(pickings, order_type='PT')
        return datas

    @post_process_and_send_data('out_stock')
    def push_scrap_to_wms(self, scraps):
        """向WMS系统推送报废单(也就是类型为XH的出库单)"""
        # self.__check_pick_type(pickings, 'outgoing')
        # datas = self.odoo_out_pickings_to_wms(scraps, order_type='XH')
        datas = self.odoo_scraps_to_wms(scraps)

        return datas

    @post_process_and_send_data('in_stock_cancel')
    def cancel_in_stocks_to_wms(self, picking, consignment_return=False):
        """
        向WMS系统取消入库单
        :param picking: 待取消的入库单
        :param consignment_return: 为True则表示是取消寄售退货单
        :return:
        """
        picking.ensure_one()
        # if not consignment_return:
            # self.__check_pick_type(picking, 'incoming')
        data = self.cancel_wms_in_picks(picking)
        return data

    @post_process_and_send_data('out_stock_cancel')
    def cancel_out_stocks_to_wms(self, picking):
        """向WMS系统取消出库单"""
        picking.ensure_one()
        # self.__check_pick_type(picking, 'outgoing')
        data = self.cancel_wms_out_picks(picking)
        return data
































